package dao;

import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Restrictions;
import org.springframework.dao.DataAccessException;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

import util.IDGenerator;
import util.SearchOperator;
import bo.PersistentObject;
import bo.User;

import com.opensymphony.xwork2.ActionContext;

import dto.SearchDTO;
import dto.SearchDTO.CriteriaDTO;
import exceptions.CannotDeleteException;

public abstract class AbstractDAO extends HibernateDaoSupport {

	/**
	 * Guarda un entitdad en la DB
	 * 
	 * @param entity	la entidad a almacenar
	 * @return			el ID de la entidad almacenada
	 */
	public String save(PersistentObject entity){
		
		User user = (User) ActionContext.getContext().getSession().get("user");
		entity.setEmpleado(user.getUsuario());
		entity.setTimestamp(new Date());

		// Save or Update
		String id = entity.getId();
		if (entity.getId() == null || "".equals(id)) {
			entity.setId(IDGenerator.generate());
			setChildIds(entity);
			beforeSave(entity);
			id = (String) getHibernateTemplate().save(entity);
		} else {
			beforeUpdate(entity);
			getHibernateTemplate().update(entity);
		}
		return id;
	}
	
	public Object merge(PersistentObject entity) {
		
		String id = entity.getId();
		if (entity.getId() == null || "".equals(id)) {
			entity.setId(IDGenerator.generate());
		}
		return getHibernateTemplate().merge(entity);
	}
	
	/**
	 * Update de una entidad
	 * @param entity	entidad a updatear
	 */
	public void update(PersistentObject entity) {
		
		getHibernateTemplate().update(entity);
	}
	
	/**
	 * Este metodo es un walkaround. Todas las clases que tienen una entidad hija, deberan implementar este
	 * metodo. 
	 * El metodo es el encargo de setear manualmente el ID de la entidad hija
	 */
	public abstract void setChildIds(PersistentObject entity);
	
	/**
	 * Metodo para llamar cuando se desea hacer algo 'justo' antes del update
	 */
	public abstract void beforeUpdate(PersistentObject entity);
	
	/**
	 * Metodo para llamar cuando se desea hacer algo 'justo' antes del save
	 */
	public abstract void beforeSave(PersistentObject entity);
	
	/**
	 * Obtiene todas las entidades de un tipo de clase
	 * @param clazz		el tipo de clase a obtener
	 * @return			el listado
	 */
	@SuppressWarnings("unchecked")
	public List<PersistentObject> findAll(Class clazz) {

		return findAll(clazz, null, false);
	}
	
	/**
	 * Obtiene todas las entidades de un tipo de clase
	 * @param clazz		el tipo de clase a obtener
	 * @param order		el atributo por el que se quiere ordenar
	 * @param asc		true si el orden tiene que ser ascendente
	 * @return			el listado
	 */
	@SuppressWarnings("unchecked")
	public List<PersistentObject> findAll(Class clazz, String order, boolean asc) {
		
		StringBuffer order_sql = new StringBuffer();
		if (order != null) {
			order_sql.append(" order by " + order);
			if (asc) {
				order_sql.append(" asc");
			} else {
				order_sql.append(" desc");
			}
		}
//		List<PersistentObject> l = (List<PersistentObject>) getHibernateTemplate().find("from " + clazz.getName() + " where deleted <> '1' " + order_sql.toString());
		List<PersistentObject> l = (List<PersistentObject>) getHibernateTemplate().find("from " + clazz.getName() + order_sql.toString());
		return l;
	}
	

	/**
	 * Obtiene una entidad segun ID
	 * @param clazz		el tipo de clase a obtener
	 * @param id		el id
	 * @return			la entidad
	 */
	public PersistentObject findById(Class clazz, String id) {
		return (PersistentObject) getHibernateTemplate().get(clazz, id);
	}
	
	
	/**
	 * Borrar una entidad
	 * @param entity	la entidad a borrar
	 * @throws CannotDeleteException 
	 */
	public void delete(PersistentObject entity) throws CannotDeleteException {
		try {
			getHibernateTemplate().delete(entity);
		} catch (DataAccessException e) {
			throw new CannotDeleteException();
		}
	}
	
	/**
	 * Busca entidades
	 * @param clazz			tipo de entidad a buscar
	 * @param searchDTO		DTO con criterios de busquedas a utilizar en la busqueda
	 * @return	la lista de entidades encontradas
	 */
	@SuppressWarnings("unchecked")
	public List<PersistentObject> search(Class clazz, SearchDTO searchDTO) {
		
		DetachedCriteria criteria = DetachedCriteria.forClass(clazz);
		
		// Search criteria
		Map<String, CriteriaDTO> criterias = searchDTO.getCriterias();
		Set<String> attributes = criterias.keySet();
		for (String attr : attributes) {
			
			CriteriaDTO criteriaDTO = criterias.get(attr);
			if (criteriaDTO.hasValues()) {

				Criterion restriction = null;
				
				String alias = criteriaDTO.getAlias();
				if (alias != null && !"".equals(alias)) {	// Criterio con alias (para busqueda sobre atributos nested
					criteria.createAlias(alias, "a");
					restriction = getRestriction("a." + attr, criteriaDTO);
				} else {
					restriction = getRestriction(attr, criteriaDTO);
				}
				criteria.add(restriction);
			}
		}

		List<PersistentObject> list = (List<PersistentObject>) getHibernateTemplate().findByCriteria(criteria);
		return list;
	}

	/**
	 * Devuelve la restriccion para un atributo
	 * @param attr	nombre del atributo
	 * @param c		mapa que contiene el valor y el operador a usar
	 */
	private Criterion getRestriction(String attr, CriteriaDTO c) {
		
		String value = "";
		if (c.getValue() != null) {
			value = c.getValue().trim();
		}
		
		if (SearchOperator.EQUAL.equals(c.getOperator())) {
			
			return Restrictions.eq(attr, value);
		}
		if (SearchOperator.LIKE.equals(c.getOperator())) {
			
			return Restrictions.ilike(attr, value, MatchMode.ANYWHERE);
		}
		if (SearchOperator.NOT_EQUAL.equals(c.getOperator())) {
			
			return Restrictions.not(Restrictions.eq(attr, value));
		}
		if (SearchOperator.START.equals(c.getOperator())) {
			
			return Restrictions.like(attr, value, MatchMode.START);
		}
		if (SearchOperator.LESS_EQ.equals(c.getOperator())) {
			
			return Restrictions.le(attr, value);
		}
		if (SearchOperator.GREATER_EQ.equals(c.getOperator())) {
			
			return Restrictions.ge(attr, value);
		}
		if (SearchOperator.BEETWEN.equals(c.getOperator())) {
			
			Criterion min = Restrictions.ge("fecha", c.getValueA());
			Criterion max = Restrictions.le("fecha", c.getValueB());
			return Restrictions.and(min, max);
		}
		// TODO para sacarlo rapido y funcionando por el momento solo funciona con 2 o 4 clausulas.
		if (SearchOperator.OR_LIKE.equals(c.getOperator())) {
			
			String[] attrs = attr.split("_");
			if (attrs.length == 2) {
				Criterion lhs = Restrictions.ilike(attrs[0], c.getValue(), MatchMode.ANYWHERE);
				Criterion rhs = Restrictions.ilike(attrs[1], c.getValue(), MatchMode.ANYWHERE);
				return Restrictions.or(lhs, rhs);
			
			} else if (attrs.length == 4) {
				Criterion c1 = Restrictions.ilike(attrs[0], c.getValue(), MatchMode.ANYWHERE);
				Criterion c2 = Restrictions.ilike(attrs[1], c.getValue(), MatchMode.ANYWHERE);
				Criterion c3 = Restrictions.ilike(attrs[2], c.getValue(), MatchMode.ANYWHERE);
				Criterion c4 = Restrictions.ilike(attrs[3], c.getValue(), MatchMode.ANYWHERE);
				return Restrictions.or(c1, Restrictions.or(c2, Restrictions.or(c3, c4)));
			}
		}
		return Restrictions.eq(attr, value);
	}
}